package com.huawang.mdesign.model.service.inter;

import cn.hutool.core.io.BomReader;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.huawang.business.util.ObjectMapperFactory;
import com.huawang.core.semantics.constants.TypeEnum;
import com.huawang.mdesign.model.service.util.LogUtil;
import no.ntnu.ihb.fmi4j.modeldescription.fmi2.Fmi2Causality;
import no.ntnu.ihb.fmi4j.modeldescription.fmi2.Fmi2ModelDescription;
import no.ntnu.ihb.fmi4j.modeldescription.fmi2.Fmi2ScalarVariable;
import org.apache.tools.zip.ZipFile;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Service;

import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Enumeration;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

@Service
@Primary
public class FmuService {

    public static final Logger log = LoggerFactory.getLogger(FmuService.class);
    private static final String DIRECTION = "direction";
    private static final String AS_PORT = "asPort";
    private static final String MODEL_NAME = "modelName";
    private static final String MODEL_IDENTIFIER = "modelIdentifier";
    private static final String FMU_VERSION = "fmuVersion";
    private static final String GUID = "guid";
    private static final String PATH = "path";
    private static final String NAME = "name";
    private static final String VALUE = "value";
    private static final String TYPE = "type";
    private static final String DESCRIPTION = "description";
    private static final String DATA = "data";
    private static final String IN = "in";
    private static final String OUT = "out";
    private static final String NA = "N/A";
    private static final String MODEL_DESCRIPTION_XML = "modelDescription.xml";
    private String projectId;

    /**
     * 解析FMU
     *
     * @param projectId String 工程ID
     * @param fmuPath   String fmu文件路径
     * @return 解析结果
     */
    @NotNull
    public ObjectNode resolveFMU(String projectId, String fmuPath) {
        LogUtil.printOnlineLogs("projectId: {}, fmuPath: {}", projectId, fmuPath);
        ObjectNode result = ObjectMapperFactory.getMapper().createObjectNode();
        ArrayNode resultData = ObjectMapperFactory.getMapper().createArrayNode();
        Fmi2ModelDescription md;
        this.projectId = projectId;
        String xml = extractModelDescriptionXmlAnt(fmuPath);
        md = Fmi2ModelDescription.fromXml(xml);

        String blockName;
        blockName = md.getModelName();
        String modelIdentifier = getModelIdentifier(md);
        String fmuVersion = md.getFmiVersion();
        String guid = md.getGuid();
        result.put(MODEL_NAME, blockName);
        result.put(MODEL_IDENTIFIER, modelIdentifier);
        result.put(FMU_VERSION, fmuVersion);
        result.put(GUID, guid);
        result.put(PATH, fmuPath);
        validateParameter(result);

        List<Fmi2ScalarVariable> variables = md.getModelVariables().getScalarVariable();
        for (Fmi2ScalarVariable variable : variables) {
            ObjectNode json = ObjectMapperFactory.getMapper().createObjectNode();
            // name
            String valueName = variable.getName() == null ? "" : variable.getName();
            json.put(NAME, valueName);
            // value
            Object value = getValue(variable, valueName);
            json.putIfAbsent(VALUE, ObjectMapperFactory.getMapper().valueToTree(value));
            // type
            String type = getType(variable);
            json.put(TYPE, type);
            // Description
            json.put(DESCRIPTION, variable.getDescription());
            initCausality(variable, json);
            // put nodeObject
            resultData.add(json);
        }
        result.putIfAbsent(DATA, resultData);
        return result;

    }

    @NotNull
    public String extractModelDescriptionXml(String path) {
        LogUtil.printOnlineLogs("path: ", path);
        try (ZipInputStream zis = new ZipInputStream(new BufferedInputStream(Files.newInputStream(Paths.get(path))))) {
            ZipEntry nextEntry;
            while ((nextEntry = zis.getNextEntry()) != null) {
                if (nextEntry.getName().equals(MODEL_DESCRIPTION_XML)) {
                    try (BufferedReader br = new BufferedReader(new BomReader(zis))) {
                        StringBuilder sb = new StringBuilder();
                        String line;
                        while ((line = br.readLine()) != null) {
                            sb.append(line).append("\n");
                        }
                        return sb.toString();
                    }
                }
            }
        } catch (IOException e) {
            throw new IllegalArgumentException(e);
        }
        throw new IllegalArgumentException("Input is not an valid FMU! No modelDescription.xml present!");
    }

    /**
     * org.apache.ant.ant读取FMU文件解决FMU文件压缩格式不一致问题
     */
    @NotNull
    public String extractModelDescriptionXmlAnt(String path) {
        LogUtil.printOnlineLogs("path: ", path);
        try (ZipFile zis = new ZipFile(path)) {
            org.apache.tools.zip.ZipEntry nextEntry;
            Enumeration<org.apache.tools.zip.ZipEntry> entries = zis.getEntries();
            while ((nextEntry = entries.nextElement()) != null) {
                if (nextEntry.getName().equals(MODEL_DESCRIPTION_XML)) {
                    try (BufferedReader br = new BufferedReader(new BomReader(zis.getInputStream(nextEntry)))) {
                        StringBuilder sb = new StringBuilder();
                        String line;
                        while ((line = br.readLine()) != null) {
                            sb.append(line).append("\n");
                        }
                        return sb.toString();
                    }
                }
            }
        } catch (IOException e) {
            throw new IllegalArgumentException(e);
        }
        throw new IllegalArgumentException("Input is not an valid FMU! No modelDescription.xml present!");
    }

    /**
     * 获取变量类型
     *
     * @param variable Fmi2ScalarVariable FMI变量对象
     * @return String 类型
     */
    @NotNull
    private static String getType(@NotNull Fmi2ScalarVariable variable) {
        LogUtil.printSerializeOnlineLogs("variable: ", variable);
        if (variable.getInteger() != null) {
            return TypeEnum.INTEGER.getName();
        } else if (variable.getReal() != null) {
            return TypeEnum.REAL.getName();
        } else if (variable.getBoolean() != null) {
            return TypeEnum.BOOLEAN.getName();
        } else if (variable.getString() != null) {
            return TypeEnum.STRING.getName();
        } else if (variable.getEnumeration() != null) {
            return TypeEnum.INSTANCE.getName();
        } else {
            return "";
        }
    }

    /**
     * 获取变量值
     *
     * @param variable  Fmi2ScalarVariable FMI变量对象
     * @param valueName String 变量名
     * @return Object 值对象
     */
    @Nullable
    private static Object getValue(@NotNull Fmi2ScalarVariable variable, String valueName) {
        LogUtil.printSerializeOnlineLogs("variable: {}, valueName: {}", variable, valueName);
        Object value;
        if (variable.getInteger() != null) {
            value = variable.getInteger().getStart();
        } else if (variable.getReal() != null) {
            if (valueName != null && valueName.startsWith("der")) {
                value = variable.getReal().getDerivative();
            } else {
                value = variable.getReal().getStart();
            }
        } else if (variable.getBoolean() != null) {
            value = variable.getBoolean();
        } else if (variable.getString() != null) {
            value = variable.getString();
        } else {
            value = null;
        }
        return value;
    }

    /**
     * 初始化因果关系
     *
     * @param variable Fmi2ScalarVariable FMI变量对象
     * @param json     ObjectNode 输出的对象节点数据
     */
    private static void initCausality(@NotNull Fmi2ScalarVariable variable, ObjectNode json) {
        LogUtil.printSerializeOnlineLogs("variable: {}, json: {}", variable, json);
        Fmi2Causality causality = variable.getCausality();
        // Direction and As Port
        if (causality == Fmi2Causality.input) {
            json.put(DIRECTION, IN);
            json.put(AS_PORT, 1);
        } else if (causality == Fmi2Causality.output) {
            json.put(DIRECTION, OUT);
            json.put(AS_PORT, 1);
        } else {
            json.put(DIRECTION, NA);
            json.put(AS_PORT, 0);
        }
    }

    /**
     * 获取模型标识信息
     *
     * @param md Fmi2ModelDescription FMI模型描述信息
     * @return String 返回模型标识信息
     */
    @NotNull
    private static String getModelIdentifier(@NotNull Fmi2ModelDescription md) {
        LogUtil.printSerializeOnlineLogs("md: ", md);
        String modelIdentifier;
        if (md.getCoSimulation() != null) {
            modelIdentifier = md.getCoSimulation().getModelIdentifier();
        } else if (md.getModelExchange() != null) {
            modelIdentifier = md.getModelExchange().getModelIdentifier();
        } else {
            modelIdentifier = "";
        }
        return modelIdentifier;
    }

    /**
     * 校验FMI文件解析关键参数
     * 关键参数:
     * modelName       String 模块名
     * modelIdentifier String 模型标识信息
     * fmuVersion      String FMU版本信息
     * guid            String FMU标识ID
     *
     * @param objectNode ObjectNode 校验的对象节点
     * @throws IllegalArgumentException 抛出文件格式错误信息
     */
    private static void validateParameter(@NotNull ObjectNode objectNode) throws IllegalArgumentException {
        LogUtil.printSerializeOnlineLogs("objectNode: ", objectNode);
        // 关键参数不可为空
        if (objectNode.get(MODEL_NAME).isNull()
                || objectNode.get(MODEL_IDENTIFIER).isNull()
                || objectNode.get(FMU_VERSION).isNull()
                || objectNode.get(GUID).isNull()) {
            throw new IllegalArgumentException("");
        }
    }

    public String getProjectId() {
        return projectId;
    }
}

